package com.mlog.njyjy.ndop.share.sql.support;

import cn.hutool.core.collection.EnumerationIter;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.ClassScaner;
import cn.hutool.core.lang.Console;
import cn.hutool.core.lang.Filter;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;

/**
 * <p>
 * 类扫描辅助工具
 * </p>
 *
 * @author <a href="mailto:yingxiufeng@mlogcn.com">alex</a>
 * @version v1.0, 2019/8/15
 */
public final class ClassScanUtil {

    private static final Logger log = LoggerFactory.getLogger(ClassScanUtil.class);

    /**
     * 文件过滤器，过滤掉不需要的文件<br>
     * 只保留Class文件、目录和Jar
     */
    private static FileFilter fileFilter = pathname -> isClass(pathname.getName()) || pathname.isDirectory() || isJarFile(pathname);

    private static final String BOOT_INF = "BOOT-INF/classes/";

    /**
     * 扫描包
     *
     * @param packageName
     * @param annotationClass
     * @param containsFatJar  兼容包路径在打包后的 fat-jar 中的情况
     * @return
     */
    public static Set<Class<?>> scanPackageByAnnotation(@NonNull String packageName,
                                                 final Class<? extends Annotation> annotationClass,
                                                 boolean containsFatJar) {
        if (!containsFatJar) {
            return ClassScaner.scanPackageByAnnotation(packageName, annotationClass);
        }
        Enumeration<URL> resources;
        try {
            resources = ClassUtil.getClassLoader().getResources(packageName.replace(StrUtil.DOT, StrUtil.SLASH));
        } catch (IOException e) {
            throw new RuntimeException(e.getLocalizedMessage());
        }
        final Set<String> classPaths = new HashSet<String>();
        String path;
        while (resources.hasMoreElements()) {
            path = resources.nextElement().getPath();
            classPaths.add(path);
        }

        final Set<Class<?>> classes = new HashSet<Class<?>>();
        classPaths.stream().forEach(log::info);
        for (String classPath : classPaths) {
            // 填充 classes
            fillClasses(classPath, packageName, clazz -> clazz.isAnnotationPresent(annotationClass), classes);
        }
        return classes;
    }

    private static void fillClasses(String path, String packageName, Filter<Class<?>> classFilter, Set<Class<?>> classes) {
        // 判定给定的路径是否为Jar
        int index = path.lastIndexOf(FileUtil.JAR_PATH_EXT);
        if (index != -1) {
            // Jar文件
            path = path.substring(
                    path.startsWith(FileUtil.PATH_FILE_PRE) ? FileUtil.PATH_FILE_PRE.length() : 0, //去除file:前缀
                    index + FileUtil.JAR_FILE_EXT.length()// 截取到.jar之后
            );
            processJarFile(new File(path), packageName, classFilter, classes);
        } else {
            fillClasses(path, new File(path), packageName, classFilter, classes);
        }
    }

    /**
     * 填充满足条件的class 填充到 classes
     *
     * @param classPath   类文件所在目录，当包名为空时使用此参数，用于截掉类名前面的文件路径
     * @param file        Class文件或者所在目录Jar包文件
     * @param packageName 需要扫面的包名
     * @param classFilter class过滤器
     * @param classes     List 集合
     */
    private static void fillClasses(String classPath, File file, String packageName, Filter<Class<?>> classFilter, Set<Class<?>> classes) {
        if (file.isDirectory()) {
            processDirectory(classPath, file, packageName, classFilter, classes);
        } else if (isClassFile(file)) {
            processClassFile(classPath, file, packageName, classFilter, classes);
        } else if (isJarFile(file)) {
            processJarFile(file, packageName, classFilter, classes);
        }
    }

    /**
     * 处理为class文件的情况,填充满足条件的class 到 classes
     *
     * @param classPath   类文件所在目录，当包名为空时使用此参数，用于截掉类名前面的文件路径
     * @param classFile   class文件
     * @param packageName 包名
     * @param classFilter 类过滤器
     * @param classes     类集合
     */
    private static void processClassFile(String classPath, File classFile, String packageName, Filter<Class<?>> classFilter, Set<Class<?>> classes) {
        if (false == classPath.endsWith(File.separator)) {
            classPath += File.separator;
        }
        String path = classFile.getAbsolutePath();
        if (StrUtil.isEmpty(packageName)) {
            path = StrUtil.removePrefix(path, classPath);
        }
        final String filePathWithDot = path.replace(File.separator, StrUtil.DOT);

        int subIndex = -1;
        if ((subIndex = filePathWithDot.indexOf(packageName)) != -1) {
            final int endIndex = filePathWithDot.lastIndexOf(FileUtil.CLASS_EXT);

            final String className = filePathWithDot.substring(subIndex, endIndex);
            fillClass(className, packageName, classes, classFilter);
        }
    }

    /**
     * 处理如果为目录的情况,需要递归调用 fillClasses方法
     *
     * @param directory   目录
     * @param packageName 包名
     * @param classFilter 类过滤器
     * @param classes     类集合
     */
    private static void processDirectory(String classPath, File directory, String packageName, Filter<Class<?>> classFilter, Set<Class<?>> classes) {
        for (File file : directory.listFiles(fileFilter)) {
            fillClasses(classPath, file, packageName, classFilter, classes);
        }
    }

    /**
     * 处理为jar文件的情况，填充满足条件的class 到 classes
     *
     * @param file        jar文件
     * @param packageName 包名
     * @param classFilter 类过滤器
     * @param classes     类集合
     */
    private static void processJarFile(File file, String packageName, Filter<Class<?>> classFilter, Set<Class<?>> classes) {
        JarFile jarFile = null;
        EnumerationIter<JarEntry> entries;
        try {
            if (!file.exists()) {
                Console.error("Jar file of [{}] not found", file.getCanonicalPath());
                return;
            }
            jarFile = new JarFile(file);
            entries = new EnumerationIter<>(jarFile.entries());
            String entryName;
            for (JarEntry jarEntry : entries) {
                entryName = jarEntry.getName();
                if (isClass(entryName)) {
                    // 这里 className 如果是 springboot 的jar 会存在 BOOT-INF/classes/
                    // 需要移除
                    if (entryName.startsWith(BOOT_INF)) {
                        entryName = entryName.replace(BOOT_INF, "");
                    }
                    final String className = StrUtil.removeSuffix(entryName.replace(StrUtil.SLASH, StrUtil.DOT), FileUtil.CLASS_EXT);
                    fillClass(className, packageName, classes, classFilter);
                } else if (isJar(entryName)) {
                    processJarStream(jarFile.getInputStream(jarEntry), packageName, classFilter, classes);
                }
            }
        } catch (Exception ex) {
            Console.error(ex, ex.getMessage());
        } finally {
            IoUtil.close(jarFile);
        }
    }

    /**
     * 填充class 到 classes
     *
     * @param className   类名
     * @param packageName 包名
     * @param classes     类集合
     * @param classFilter 类过滤器
     */
    private static void fillClass(String className, String packageName, Set<Class<?>> classes, Filter<Class<?>> classFilter) {
        if (className.startsWith(packageName)) {
            try {
                final Class<?> clazz = Class.forName(className, false, ClassUtil.getClassLoader());
                if (classFilter == null || classFilter.accept(clazz)) {
                    classes.add(clazz);
                }
            } catch (Throwable ex) {
                // Pass Load Error.
            }
        }
    }

    /**
     * 处理为jar文件的情况，填充满足条件的class 到 classes
     *
     * @param in          jar文件流
     * @param packageName 包名
     * @param classFilter 类过滤器
     * @param classes     类集合
     */
    private static void processJarStream(InputStream in, String packageName, Filter<Class<?>> classFilter, Set<Class<?>> classes) {
        JarInputStream jarIn = null;
        try {
            jarIn = (in instanceof JarInputStream) ? (JarInputStream) in : new JarInputStream(in);
            JarEntry entry;
            String entryName;
            while (null != (entry = jarIn.getNextJarEntry())) {
                entryName = entry.getName();
                if (isClass(entryName)) {
                    final String className = StrUtil.removeSuffix(entryName.replace(StrUtil.SLASH, StrUtil.DOT), FileUtil.CLASS_EXT);
                    fillClass(className, packageName, classes, classFilter);
                }
            }
        } catch (Exception ex) {
            Console.error(ex, ex.getMessage());
        } finally {
            IoUtil.close(jarIn);
            IoUtil.close(in);
        }
    }

    /**
     * @param file 文件
     * @return 是否为类文件
     */
    private static boolean isClassFile(File file) {
        return isClass(file.getName());
    }

    /**
     * @param fileName 文件名
     * @return 是否为类文件
     */
    private static boolean isClass(String fileName) {
        return fileName.endsWith(FileUtil.CLASS_EXT);
    }

    /**
     * @param file 文件
     * @return 是否为Jar文件
     */
    private static boolean isJarFile(File file) {
        return isJar(file.getName());
    }

    /**
     * @param fileName 文件名
     * @return 是否为Jar文件
     */
    private static boolean isJar(String fileName) {
        return fileName.endsWith(FileUtil.JAR_FILE_EXT);
    }
}
